Skip to content

[ty] Handle most "deep" mutual typevar constraints#24079

Open
dcreager wants to merge 20 commits intomainfrom
dcreager/deep-bounds
Open

[ty] Handle most "deep" mutual typevar constraints#24079
dcreager wants to merge 20 commits intomainfrom
dcreager/deep-bounds

Conversation

@dcreager
Copy link
Copy Markdown
Member

@dcreager dcreager commented Mar 20, 2026

This is the first step in supporting astral-sh/ty#2045. It handles all variances, but some of the mdtests still have TODOs because they will also require updating SpecializationBuilder to combine constraint sets across multiple arguments in a call.

For the covariant case, we have:

def invoke[A, B](fn: Callable[[A], B], value: A) -> B:
    return fn(value)

def head[T](xs: Sequence[T]) -> T: ...
def lift[T](x: T) -> Sequence[T]: ...

reveal_type(invoke(head, [1, 2, 3]))  # revealed: int
reveal_type(invoke(lift, 1))  # revealed: Sequence[Literal[1]]

With this PR, the first call is still TODO, but the second call is now revealed correctly.

There are also several lower-level mdtests on the constraint set implementation itself, testing that we actually detect the necessary implications correctly.

@astral-sh-bot astral-sh-bot bot added the ty Multi-file analysis & type inference label Mar 20, 2026
@astral-sh-bot astral-sh-bot bot requested a review from oconnor663 March 20, 2026 14:10
@dcreager dcreager added ty Multi-file analysis & type inference and removed ty Multi-file analysis & type inference labels Mar 20, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 20, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 86.38%. The percentage of expected errors that received a diagnostic held steady at 80.68%. The number of fully passing files held steady at 67/133.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 20, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 712.98MB 713.43MB +0.06% (462.85kB)
sphinx 265.09MB 265.18MB +0.03% (92.96kB)
trio 116.82MB 116.85MB +0.03% (35.88kB)
flake8 48.44MB 48.45MB +0.02% (11.80kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_definition_types 89.85MB 89.95MB +0.11% (104.03kB)
infer_expression_types_impl 56.19MB 56.29MB +0.17% (100.22kB)
infer_expression_type_impl 14.05MB 14.10MB +0.36% (52.45kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 10.11MB 10.14MB +0.35% (36.64kB)
StaticClassLiteral<'db>::variance_of_ 180.16kB 215.23kB +19.46% (35.07kB)
StaticClassLiteral<'db>::variance_of_::interned_arguments 25.10kB 54.28kB +116.25% (29.18kB)
Type<'db>::member_lookup_with_policy_ 16.31MB 16.33MB +0.15% (25.27kB)
infer_scope_types_impl 53.63MB 53.66MB +0.05% (24.81kB)
GenericAlias<'db>::variance_of_ 573.88kB 589.88kB +2.79% (16.00kB)
Type<'db>::class_member_with_policy_ 17.76MB 17.78MB +0.08% (14.54kB)
GenericAlias<'db>::variance_of_::interned_arguments 21.30kB 34.03kB +59.74% (12.73kB)
all_narrowing_constraints_for_expression 7.42MB 7.43MB +0.15% (11.60kB)
is_redundant_with_impl::interned_arguments 5.41MB 5.40MB -0.18% (10.23kB)
is_redundant_with_impl 5.51MB 5.50MB -0.10% (5.39kB)
all_negative_narrowing_constraints_for_expression 2.81MB 2.81MB +0.17% (4.85kB)
... 23 more

sphinx

Name Old New Diff Outcome
infer_expression_types_impl 19.69MB 19.71MB +0.12% (23.82kB)
infer_definition_types 24.13MB 24.15MB +0.09% (22.11kB)
StaticClassLiteral<'db>::variance_of_ 9.66kB 22.67kB +134.79% (13.02kB)
StaticClassLiteral<'db>::variance_of_::interned_arguments 7.66kB 18.84kB +145.87% (11.18kB)
GenericAlias<'db>::variance_of_ 6.43kB 15.70kB +144.08% (9.27kB)
GenericAlias<'db>::variance_of_::interned_arguments 5.98kB 13.01kB +117.65% (7.03kB)
infer_scope_types_impl 15.52MB 15.52MB +0.04% (5.60kB)
Type<'db>::member_lookup_with_policy_ 6.51MB 6.51MB +0.01% (456.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 2.40MB 2.40MB +0.02% (456.00B)
is_redundant_with_impl::interned_arguments 2.02MB 2.02MB -0.02% (440.00B)
infer_expression_type_impl 3.58MB 3.58MB +0.01% (408.00B)
infer_deferred_types 5.61MB 5.61MB +0.01% (360.00B)
function_known_decorators 2.46MB 2.46MB +0.01% (240.00B)
is_redundant_with_impl 1.78MB 1.78MB -0.01% (240.00B)
UnionType<'db>::from_two_elements_::interned_arguments 783.32kB 783.15kB -0.02% (176.00B)
... 1 more

trio

Name Old New Diff Outcome
StaticClassLiteral<'db>::variance_of_ 6.63kB 17.62kB +165.67% (10.99kB)
StaticClassLiteral<'db>::variance_of_::interned_arguments 5.20kB 14.62kB +181.08% (9.42kB)
infer_definition_types 7.49MB 7.50MB +0.09% (6.83kB)
infer_expression_types_impl 6.03MB 6.04MB +0.06% (3.82kB)
GenericAlias<'db>::variance_of_ 3.95kB 6.55kB +65.88% (2.60kB)
is_redundant_with_impl::interned_arguments 533.93kB 531.61kB -0.43% (2.32kB)
GenericAlias<'db>::variance_of_::interned_arguments 3.59kB 5.55kB +54.90% (1.97kB)
CallableType 481.56kB 482.90kB +0.28% (1.34kB)
is_redundant_with_impl 469.91kB 468.80kB -0.24% (1.11kB)
infer_scope_types_impl 4.76MB 4.76MB +0.01% (492.00B)
InferableTypeVarsInner 76.55kB 76.89kB +0.44% (344.00B)
InferableTypeVars<'db>::merge_::interned_arguments 13.64kB 13.92kB +2.06% (288.00B)
InferableTypeVars<'db>::merge_ 10.93kB 11.17kB +2.22% (248.00B)
infer_expression_type_impl 1.41MB 1.41MB +0.01% (216.00B)
GenericContext 124.96kB 125.15kB +0.15% (196.00B)
... 9 more

flake8

Name Old New Diff Outcome
StaticClassLiteral<'db>::variance_of_ 1.66kB 4.84kB +190.61% (3.17kB)
StaticClassLiteral<'db>::variance_of_::interned_arguments 1.62kB 4.22kB +160.87% (2.60kB)
infer_definition_types 1.92MB 1.92MB +0.08% (1.56kB)
infer_expression_types_impl 1.03MB 1.03MB +0.13% (1.38kB)
GenericAlias<'db>::variance_of_ 1.48kB 2.29kB +54.76% (828.00B)
infer_scope_types_impl 997.85kB 998.55kB +0.07% (720.00B)
function_known_decorators 316.92kB 317.41kB +0.16% (504.00B)
FunctionType 436.50kB 436.94kB +0.10% (448.00B)
GenericAlias<'db>::variance_of_::interned_arguments 1.34kB 1.76kB +31.58% (432.00B)
OverloadLiteral 119.96kB 120.18kB +0.18% (224.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 20, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
type-assertion-failure 0 0 19
invalid-argument-type 11 1 4
invalid-return-type 2 2 1
unresolved-attribute 2 0 0
no-matching-overload 1 0 0
unsupported-operator 1 0 0
Total 17 3 24
Raw diff (44 changes)
Tanjun (https://github.com/FasterSpeeding/Tanjun)
- tanjun/dependencies/data.py:220:12 error[invalid-return-type] Return type does not match returned value: expected `_T@inject_lc`, found `_T@inject_lc | Coroutine[Any, Any, _T@inject_lc]`

anyio (https://github.com/agronholm/anyio)
+ src/anyio/from_thread.py:378:60 error[invalid-argument-type] Argument to bound method `_spawn_task_from_thread` is incorrect: Expected `Future[T_Retval@start_task_soon | Awaitable[T_Retval@start_task_soon]]`, found `Future[T_Retval@start_task_soon]`

bokeh (https://github.com/bokeh/bokeh)
- src/bokeh/layouts.py:671:16 error[invalid-return-type] Return type does not match returned value: expected `list[L@_parse_children_arg]`, found `list[L@_parse_children_arg | list[L@_parse_children_arg]]`
+ src/bokeh/layouts.py:671:21 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Iterable[L@_parse_children_arg]`, found `tuple[L@_parse_children_arg | list[L@_parse_children_arg], ...]`

more-itertools (https://github.com/more-itertools/more-itertools)
- more_itertools/recipes.py:1111:18 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(int | float | complex | ... omitted 3 union elements, int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], /) -> int | float | complex | ... omitted 3 union elements`, found `Overload[(base: int, exp: int, mod: int) -> int, (base: int, exp: Literal[0], mod: None = None) -> Literal[1], (base: int, exp: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], mod: None = None) -> int, (base: int, exp: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], mod: None = None) -> int | float, (base: int, exp: int, mod: None = None) -> Any, (base: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], exp: int | float, mod: None = None) -> int | float, (base: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], exp: int | float, mod: None = None) -> int | float | complex, (base: int | float, exp: int, mod: None = None) -> int | float, (base: int | float, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> Any, (base: int | float | complex, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> int | float | complex, [_E_contra, _T_co](base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _T_co](base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _M_contra, _T_co](base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float, mod: None = None) -> Any, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float | complex, mod: None = None) -> int | float | complex]`
+ more_itertools/recipes.py:1111:18 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(int | float | complex | ... omitted 4 union elements, int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], /) -> int | float | complex | ... omitted 4 union elements`, found `Overload[(base: int, exp: int, mod: int) -> int, (base: int, exp: Literal[0], mod: None = None) -> Literal[1], (base: int, exp: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], mod: None = None) -> int, (base: int, exp: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], mod: None = None) -> int | float, (base: int, exp: int, mod: None = None) -> Any, (base: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], exp: int | float, mod: None = None) -> int | float, (base: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], exp: int | float, mod: None = None) -> int | float | complex, (base: int | float, exp: int, mod: None = None) -> int | float, (base: int | float, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> Any, (base: int | float | complex, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> int | float | complex, [_E_contra, _T_co](base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _T_co](base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _M_contra, _T_co](base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float, mod: None = None) -> Any, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float | complex, mod: None = None) -> int | float | complex]`

mypy (https://github.com/python/mypy)
+ mypy/build.py:4537:38 error[invalid-argument-type] Argument to function `is_sub_path_normabs` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/build.py:4539:42 error[invalid-argument-type] Argument to function `is_sub_path_normabs` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/dmypy_server.py:1111:39 error[invalid-argument-type] Argument to bound method `listdir` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/modulefinder.py:49:16 error[invalid-return-type] Return type does not match returned value: expected `dict[str, tuple[str, ...]]`, found `dict[str, tuple[PathLike[PathLike[Never] | str] | str, ...] | Unknown]`
+ mypy/modulefinder.py:457:67 error[invalid-argument-type] Argument to bound method `find_lib_path_dirs` is incorrect: Expected `tuple[str, ...]`, found `Unknown | tuple[PathLike[PathLike[Never] | str] | str, ...]`
+ mypy/modulefinder.py:466:37 error[invalid-argument-type] Argument to function `os_path_join` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/modulefinder.py:470:24 error[no-matching-overload] No overload of function `join` matches arguments
+ mypy/modulefinder.py:478:57 error[invalid-argument-type] Argument to function `os_path_join` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/modulefinder.py:489:68 error[invalid-argument-type] Argument to bound method `_find_module_non_stub_helper` is incorrect: Expected `str`, found `Unknown | PathLike[PathLike[Never] | str] | str`
+ mypy/modulefinder.py:504:59 error[invalid-argument-type] Argument to bound method `find_lib_path_dirs` is incorrect: Expected `tuple[str, ...]`, found `Unknown | tuple[PathLike[PathLike[Never] | str] | str, ...]`
+ mypy/modulefinder.py:509:64 error[invalid-argument-type] Argument to bound method `find_lib_path_dirs` is incorrect: Expected `tuple[str, ...]`, found `Unknown | tuple[PathLike[PathLike[Never] | str] | str, ...]`

prefect (https://github.com/PrefectHQ/prefect)
+ src/prefect/_internal/concurrency/api.py:41:16 error[invalid-return-type] Return type does not match returned value: expected `Call[T@cast_to_call]`, found `Call[Awaitable[T@cast_to_call] | T@cast_to_call]`
- src/prefect/input/run_input.py:672:20 error[invalid-return-type] Return type does not match returned value: expected `T@GetAutomaticInputHandler | AutomaticRunInput[T@GetAutomaticInputHandler]`, found `Unknown | Coroutine[Any, Any, Unknown]`
+ src/prefect/input/run_input.py:672:20 error[invalid-return-type] Return type does not match returned value: expected `T@GetAutomaticInputHandler | AutomaticRunInput[T@GetAutomaticInputHandler]`, found `T@GetAutomaticInputHandler | AutomaticRunInput[T@GetAutomaticInputHandler] | Coroutine[Any, Any, T@GetAutomaticInputHandler | AutomaticRunInput[T@GetAutomaticInputHandler]]`

scipy (https://github.com/scipy/scipy)
+ scipy/optimize/_trustregion_exact.py:367:40 error[unsupported-operator] Operator `**` is not supported between objects of type `Unknown | SupportsAbs[SupportsDunderLT[Any] | SupportsDunderGT[Any]]` and `Literal[2]`

scipy-stubs (https://github.com/scipy/scipy-stubs)
- tests/sparse/test_construct.pyi:236:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:236:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:237:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:237:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:238:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `csc_array[complexfloating[_32Bit, _32Bit]]`
+ tests/sparse/test_construct.pyi:238:1 error[type-assertion-failure] Type `csc_array[numpy.bool[builtins.bool]]` does not match asserted type `csc_array[complexfloating[_32Bit, _32Bit]]`
- tests/sparse/test_construct.pyi:239:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `csr_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:239:1 error[type-assertion-failure] Type `csr_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `csr_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:240:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:240:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:241:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:241:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:242:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:242:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:244:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[floating[_32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:244:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[floating[_32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:245:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[float64, tuple[int, int]]`
+ tests/sparse/test_construct.pyi:245:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[float64, tuple[int, int]]`
- tests/sparse/test_construct.pyi:246:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `csc_array[float64]`
+ tests/sparse/test_construct.pyi:246:1 error[type-assertion-failure] Type `csc_array[numpy.bool[builtins.bool]]` does not match asserted type `csc_array[float64]`
- tests/sparse/test_construct.pyi:247:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `csr_array[float64, tuple[int, int]]`
+ tests/sparse/test_construct.pyi:247:1 error[type-assertion-failure] Type `csr_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `csr_array[float64, tuple[int, int]]`
- tests/sparse/test_construct.pyi:248:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complex128, tuple[int, int]]`
+ tests/sparse/test_construct.pyi:248:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complex128, tuple[int, int]]`
- tests/sparse/test_construct.pyi:249:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complex128, tuple[int, int]]`
+ tests/sparse/test_construct.pyi:249:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complex128, tuple[int, int]]`
- tests/sparse/test_construct.pyi:250:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complex128, tuple[int, int]]`
+ tests/sparse/test_construct.pyi:250:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complex128, tuple[int, int]]`
- tests/sparse/test_construct.pyi:268:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[signedinteger[_64Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:268:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[signedinteger[_64Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:269:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `csr_array[floating[_32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:269:1 error[type-assertion-failure] Type `csr_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `csr_array[floating[_32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:270:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:270:1 error[type-assertion-failure] Type `coo_array[numpy.bool[builtins.bool], tuple[int, int]]` does not match asserted type `coo_array[complexfloating[_32Bit, _32Bit], tuple[int, int]]`
- tests/sparse/test_construct.pyi:280:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_matrix[signedinteger[_64Bit]]`
+ tests/sparse/test_construct.pyi:280:1 error[type-assertion-failure] Type `coo_matrix[Unknown]` does not match asserted type `coo_matrix[signedinteger[_64Bit]]`
- tests/sparse/test_construct.pyi:282:1 error[type-assertion-failure] Type `Unknown` does not match asserted type `coo_array[signedinteger[_64Bit], tuple[int, int]]`
+ tests/sparse/test_construct.pyi:282:1 error[type-assertion-failure] Type `coo_array[Unknown, tuple[int, int]]` does not match asserted type `coo_array[signedinteger[_64Bit], tuple[int, int]]`

setuptools (https://github.com/pypa/setuptools)
- setuptools/_distutils/extension.py:123:37 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str | bytes | PathLike[str], /) -> str | bytes | PathLike[str]`, found `Overload[(path: str) -> str, (path: bytes) -> bytes, [AnyStr](path: PathLike[AnyStr]) -> AnyStr]`
+ setuptools/_distutils/extension.py:123:37 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str | bytes | PathLike[str | bytes], /) -> str | bytes | PathLike[str | bytes | PathLike[str | bytes]]`, found `Overload[(path: str) -> str, (path: bytes) -> bytes, [AnyStr](path: PathLike[AnyStr]) -> AnyStr]`
- setuptools/_vendor/more_itertools/recipes.py:1174:18 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(int | float | complex | ... omitted 3 union elements, int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], /) -> int | float | complex | ... omitted 3 union elements`, found `Overload[(base: int, exp: int, mod: int) -> int, (base: int, exp: Literal[0], mod: None = None) -> Literal[1], (base: int, exp: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], mod: None = None) -> int, (base: int, exp: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], mod: None = None) -> int | float, (base: int, exp: int, mod: None = None) -> Any, (base: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], exp: int | float, mod: None = None) -> int | float, (base: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], exp: int | float, mod: None = None) -> int | float | complex, (base: int | float, exp: int, mod: None = None) -> int | float, (base: int | float, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> Any, (base: int | float | complex, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> int | float | complex, [_E_contra, _T_co](base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _T_co](base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _M_contra, _T_co](base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float, mod: None = None) -> Any, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float | complex, mod: None = None) -> int | float | complex]`
+ setuptools/_vendor/more_itertools/recipes.py:1174:18 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(int | float | complex | ... omitted 4 union elements, int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], /) -> int | float | complex | ... omitted 4 union elements`, found `Overload[(base: int, exp: int, mod: int) -> int, (base: int, exp: Literal[0], mod: None = None) -> Literal[1], (base: int, exp: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], mod: None = None) -> int, (base: int, exp: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], mod: None = None) -> int | float, (base: int, exp: int, mod: None = None) -> Any, (base: Literal[1, 2, 3, 4, 5, ... omitted 20 literals], exp: int | float, mod: None = None) -> int | float, (base: Literal[-1, -2, -3, -4, -5, ... omitted 15 literals], exp: int | float, mod: None = None) -> int | float | complex, (base: int | float, exp: int, mod: None = None) -> int | float, (base: int | float, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> Any, (base: int | float | complex, exp: int | float | complex | _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = None) -> int | float | complex, [_E_contra, _T_co](base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _T_co](base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co, [_E_contra, _M_contra, _T_co](base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float, mod: None = None) -> Any, (base: _SupportsPow2[Any, Any] | _SupportsPow3[Any, Any, Any], exp: int | float | complex, mod: None = None) -> int | float | complex]`
- setuptools/command/editable_wheel.py:420:19 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str | bytes, /) -> str | bytes`, found `Overload[(path: str) -> str, (path: bytes) -> bytes, [AnyStr](path: PathLike[AnyStr]) -> AnyStr]`
+ setuptools/command/editable_wheel.py:420:19 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str | bytes | PathLike[str | bytes], /) -> str | bytes | PathLike[str | bytes | PathLike[str | bytes]]`, found `Overload[(path: str) -> str, (path: bytes) -> bytes, [AnyStr](path: PathLike[AnyStr]) -> AnyStr]`
- setuptools/command/editable_wheel.py:420:30 error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[str | bytes]`, found `Unknown | list[Path]`

sympy (https://github.com/sympy/sympy)
+ sympy/polys/matrices/sdm.py:1745:36 error[unresolved-attribute] Attribute `items` is not defined on `Iterable[SupportsDunderLT[Any] | SupportsDunderGT[Any]]` in union `Unknown | Iterable[SupportsDunderLT[Any] | SupportsDunderGT[Any]]`
+ sympy/polys/matrices/sdm.py:1954:36 error[unresolved-attribute] Object of type `Iterable[SupportsDunderLT[Any] | SupportsDunderGT[Any]]` has no attribute `items`

Full report with detailed diff (timing results)

@dcreager
Copy link
Copy Markdown
Member Author

Moving this to draft while I investigate the perf regression

@dcreager dcreager force-pushed the dcreager/deep-bounds branch from 8570b68 to 001b57d Compare March 21, 2026 18:52
@dcreager dcreager force-pushed the dcreager/deep-bounds branch from 6c54a41 to ca9105a Compare March 23, 2026 17:57
@dcreager
Copy link
Copy Markdown
Member Author

The ecosystem hits all look like they are new false positives, due to us still not combining constraint sets across multiple arguments. I've added some mdtests that mimic those new diagnostics.

@dcreager dcreager marked this pull request as ready for review March 23, 2026 17:59
@carljm carljm removed their request for review March 23, 2026 19:51
Copy link
Copy Markdown
Contributor

@oconnor663 oconnor663 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've spent some time reading through this and talking to @dcreager, but my understanding of what's going on still isn't very good, and I'm afraid I don't have any useful feedback. @carljm or @sharkdp, would one of you have time to review before this goes in?

}
}

fn add_sequents_for_pair<'db>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not new in this PR, but in the spirit of trying to ask at least one question at the limit of my understanding: Are there cases where analyzing constraints one-pair-at-a-time isn't sufficient? Like could there be a conclusion that we can only draw by looking at three constraints together?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of right now, no, but that is definitely a limitation. So far everything that might require three constraints can proceed in two steps, each of which depends on only two constraints.

That's actually one of the things that I'm bumping into in #22400, since there are currently some patterns that check an equality constraint and some other constraint, and breaking that equality constraint into separate lower and upper bound constraints would mean we'd need to check three constraints at once for that pattern. And I'm loath to introduce a cubic loop like that.

@oconnor663 oconnor663 removed their assignment Mar 31, 2026
@carljm carljm self-assigned this Mar 31, 2026
Copy link
Copy Markdown
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting with comments -- it seems like the failed-assertion panic case should be fixed before landing.

// `lower ≤ upper`?") rather than a universal check, because the bounds may mention
// typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable when `int ≤ T`.
if lower
.when_constraint_set_assignable_to(db, upper, builder)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to use the builder here before we've interned the constraint typevars below? Seems like it might mean that we're operating on an incomplete typevar ordering, leading to inconsistencies later on?

// Fast path: If L is trivially always assignable to U, there are no derived constraints
// that we can infer. (This would be handled correctly by the logic below, but this is a
// useful early return.)
if when.node == ALWAYS_TRUE {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you decide when to do this manual check vs when.is_always_satisfied(db)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a comment to this effect, but to answer here as well: (1) This is an early return as an optimization, so it's okay to use the cheaper check. And (2) is_always_satisfied takes into account derived facts, by pruning paths that are impossible because they contain contradictory constraints. Here we're in the middle of building up those derived facts, so it's especially not worth performing the more expensive check if the necessary data is still incomplete.

}
// If L is _never_ assignable to U, this constraint would violate transitivity, and should
// never have been added.
debug_assert!(!when.is_never_satisfied(db));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex came up with this mdtest repro which panics here on debug builds:

from typing import Never, TypeVar, Union
from ty_extensions import ConstraintSet, static_assert

A = TypeVar("A")
T = TypeVar("T")
U = TypeVar("U")

constraints = (
    ConstraintSet.range(int, A, Union[T, U])
    & ConstraintSet.range(Never, T, str)
    & ConstraintSet.range(Never, U, bytes)
)

static_assert(not constraints)

The issue seems to be that in ConstraintId::new_node and ConstraintId::intersect, we check that L <= U is existentially satisfiable -- but that check doesn't aim to be comprehensive, it assumes that other constraints (upper bounds of T and U in the above repro) will be independently added to the constraint set. So those checks say "sure, (int <= T <= object) OR (int <= U <= object) looks existentially satisfiable", without considering that actually both int <= T and int <= U are impossible due to the upper bounds of T and U.

But then later here, once typevar bounds are added to the constraint set, we end up handling the derived constraint int <= A <= str | bytes, and panicking on this assertion because int <= str | bytes is never satisfiable.

Codex suggests that this should just be an early return rather than an assertion; not sure if you think a better fix is needed.

It seems like we should reject the above case as never satisfiable, though, not just fail to add sequents for it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then later here, once typevar bounds are added to the constraint set, we end up handling the derived constraint int <= A <= str | bytes, and panicking on this assertion because int <= str | bytes is never satisfiable.

Codex suggests that this should just be an early return rather than an assertion; not sure if you think a better fix is needed.

It seems like we should reject the above case as never satisfiable, though, not just fail to add sequents for it?

Ooh this is a great catch. Yes I agree that this should be treated as never satisfiable. In fact, we should notice that int ≤ A ≤ str | bytes is on its own never satisfiable before we ever get to this point. This seems like it could be a more fundamental check to add — if we ever try to create a pair implication where the rhs is false, that's actually a pair impossibility. I think that would end up catching this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants